Skip to content

fix(driver-memory): a resolved dateRange window's upper bound is exclusive, so 'today' stops before tomorrow - #16966

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-16179-analytics-daterange-upper-bound
Sep 8, 2026
Merged

fix(driver-memory): a resolved dateRange window's upper bound is exclusive, so 'today' stops before tomorrow#16966
os-musk merged 2 commits into
mainfrom
claude/issue-16179-analytics-daterange-upper-bound

Conversation

@os-musk

@os-musk os-musk commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16179

Clause-②: no

Route 2, as triage settled it: fix what the relative tokens emit, and leave an explicit dateRange: [a, b] alone.

The defect, re-measured

parseDateRangeString('today') returns the day's start instant and the next day's start instant. The analytics call site compared that upper bound with $lte, because nextUtcCalendarDay widens only a bare YYYY-MM-DD and returns null for a full timestamp, so the half-open branch was never taken. 'today' was therefore one day plus one instant long: two adjacent day windows overlapped at midnight and a row stamped exactly there was counted in both.

Driven end to end through MemoryAnalyticsService.query() against the built package, the double-count as a number rather than an argument — the same row, the same table, asked on two consecutive days:

AssertionError: a row must belong to exactly one day — 2 is the double-count this card is about: expected 2 to be 1
- Expected   1
+ Received   2

The repair

parseDateRangeString now returns the window and what its upper bound means, and only a window this driver resolved is compared with $lt:

The union's two arms are discriminated by Array.isArray one line above the bound construction, so the token path and the explicit-array path never had to share an answer. ⇒ the escalation clause did not fire.

⚠️ The other spelling of route 2 — having the token emit a bare YYYY-MM-DD end — is not available on this path, and its failure would have been silent. boundary() renders the query timezone's midnight instant; a bare day would be widened and then cut at T00:00:00Z, i.e. at UTC midnight, undoing #16042 for every non-UTC caller. Measured on Asia/Shanghai: the correct window ends at 2026-09-06T16:00:00.000Z, and the bare-day route would end it eight hours late.

⭐ The control: the explicit array is identical to the character

Triage's second acceptance half is a measurement, not an assertion. The card's own three-row fixture, through the built package's public entry, before and after the repair:

section sha256 of the captured output verdict
explicit ['2026-09-06T00:00:00.000Z','2026-09-07T00:00:00.000Z'], both storage forms 2130b5b81b7965212fbcdb3bc3e6dad4f57c2b74c3372625dcad47021e525a46 before and after byte-identical, diff exit 0
token 'today', both storage forms differs the next-day 00:00:00.000 row is dropped, and only it

The published type surface is byte-identical too: dist/index.d.ts sha256 2be0755c20d8eadf59938f00ddf78f97c69642c84607f6ef7935fff4a31ad476 with the fix and with memory-analytics.ts reverted to origin/main and rebuilt. No exported symbol, no option key — the carrier is unchanged.

Ablation

Mutation: endExclusive: truefalse in the 'today' branch — the minimal restoration of the defect, leaving the array path alone. On-disk proof each leg: occurrence counts of the exact texts flipped 1→0 and 0→1, and the blob hash moved off the HEAD blob (0315a768…22c7fdaf…). Restore verified by blob-hash equality and an empty git diff HEAD, under an EXIT INT TERM trap on absolute paths.

Predicted direction: red on the token leg, green on the explicit-array leg. Observed, at HEAD 17921d98d5:

Tests  18 failed | 12 passed (30)

The 18 are exactly the token assertions (8 boundary-row + 8 two-arms-differ + 2 partition). The 12 that stay green include all 8 explicit-array control cases; re-run alone under the same mutation they are 8 passed | 22 skipped. That is the route-1 tripwire: a repair that made $lt unconditional reddens there, not here.

⚠️ The first ablation attempt is NOT MEASURED, not a red: it carried --reporter=basic, which vitest 4 tried to load as a custom reporter module (ERR_LOAD_URL), so the suite never started. Re-run without it.

Verification

Exit codes captured by redirect-then-$?, never across a pipe.

COVERAGE — gates derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the final head 17921d98d5 (change set: the 3 files in this PR; identical to the derivation taken before the last commit). All 55 invoked; reconciled with --ran:

✓ dispatch-gates --ran: 55 derived famil(ies) accounted for — 55 run, 0 NOT-MEASURED.

VERDICTS — stated separately from coverage, because they are not the same fact:

  • 53 green.
  • 2 NOT MEASURED, exit 3 PREREQUISITE NOT MET — ⛔ neither is a pass: pnpm check:dual-build-cjs-loads (81 packages have no dist/) and pnpm check:type-check-debt (27 workspace deps of the ledgered packages have no built type entry point). Both demand a whole-monorepo build closure, which is a repo-wide run CI owns; declared to CI, and no ledger number is read from either.
  • 0 red.

Package-level, under the shared verify lock:

command verdict
pnpm --filter @objectstack/driver-memory typecheck green (tsc --noEmit + tsconfig.typecheck.json)
pnpm --filter @objectstack/driver-memory test Test Files 46 passed (46) · Tests 1148 passed | 15 todo (1163)
pnpm --filter '@objectstack/driver-memory...' build green
pnpm lint (repo-wide, eslint . --no-inline-config) green, exit 0, 111s wall on a shared box — the full population, ⛔ not a narrowing

The new pin file is reached by the package's tsconfig.json (tsc --listFiles), so its typecheck is not blind to it.

Anchors, re-measured, with the tree named

Every anchor below was taken on the worktree /home/user/objectstack-issue-16179 at 97adce2faa9d27d7811f1f299f5ed806a467f624, which is origin/main. Triage did not re-check any of them.

cited measured verdict
memory-analytics.ts:760const nextDay = nextUtcCalendarDay(end); :766 ⚠️ stale by 6; the quoted shape is verbatim correct
calendar-day.ts:42 — the documented contract :42, export function nextUtcCalendarDay(value: unknown): string | null ✅ exact
calendar-day.test.ts:29-31 — the pin :29-31, the three instant refusals inside it('refuses instants …') at :28 ✅ exact
the three-row public-entry measurement reproduced against the built package, both storage forms ✅ reproduces, 'today' too

The premise holds. 'today' is in DATE_RANGE_PRESETS, so it survives the closed vocabulary and reaches the resolver — the token path is live.

验收备注

Changeset

patch, measured rather than assumed: @objectstack/driver-memory is 17.3.0 with real exports, publishConfig.access: public, no private: true, and it sits in .changeset/config.json's fixed group with ignore: []. It publishes ⇒ a bug fix in it takes a patch changeset and ⛔ never skip-changeset — AGENTS.md :1036 on origin/main.


Generated by Claude Code

…exclusive

`parseDateRangeString('today')` returns the day's start instant and the NEXT
day's start instant, and the analytics call site compared that upper bound with
`$lte` — `nextUtcCalendarDay` widens only a bare `YYYY-MM-DD` and returns null
for an instant, so the half-open branch was never taken. `'today'` was one day
plus one instant long, two adjacent day windows overlapped at midnight, and a
row stamped there was counted in both.

The resolver now states whether the bound it produced is exclusive, and only a
window this driver resolved is compared with `$lt`. An explicit
`dateRange: [a, b]` is untouched: a caller-written timestamp end keeps the
inclusive reading this package publishes today, and a bare-day end keeps its
whole-day widening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
The `Intl.DateTimeFormat` fence dropped `fractionalSecondDigits` — it is not in
this package's `lib` view of `DateTimeFormatOptions` — and reads the sub-second
half off the window literal instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
@github-actions github-actions Bot added the size/m label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 22c0279fc83224831581c0c83e0dd9e5fa82d5ddpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 6f190d4ffb339300ee4f5e91b99726280794b5ef — the merge of head 17921d98d56479eab09e080a9207b75e58735557 into base 22c0279fc83224831581c0c83e0dd9e5fa82d5dd, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6f190d4ffb339300ee4f5e91b99726280794b5ef && git checkout 6f190d4ffb339300ee4f5e91b99726280794b5ef
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 22c0279fc83224831581c0c83e0dd9e5fa82d5dd 17921d98d56479eab09e080a9207b75e58735557 && git checkout -B drift-repro 22c0279fc83224831581c0c83e0dd9e5fa82d5dd && git merge --no-ff 17921d98d56479eab09e080a9207b75e58735557

node scripts/docs-audit/affected-docs.mjs --json 22c0279fc83224831581c0c83e0dd9e5fa82d5dd

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

driver-memory analytics dateRange upper bound is INCLUSIVE for a full-timestamp end, so the first instant of tomorrow counts as today

2 participants